using System;
using System.Collections.Generic;
using System.Text;

using Mono.Cecil;
using Mono.Cecil.Cil;

using CILFlow;
using CILUtil;
using CILDiffLibPrime.Attributes;

using CILDiffLibPrime.Collectors;

namespace CILDiffLibPrime
{
    public class Visit
    {
        List<AbstractCollector> m_collecterList = new List<AbstractCollector>();
        Dictionary<String,AbstractCollector> m_registeredCollectors = new Dictionary<String, AbstractCollector>();

        AssemblyDefinition m_merge;
        public void CompareFiles(string fileA, string fileB)
        {
            AssemblyDefinition a = LoadFile(fileA);
            AssemblyDefinition b = LoadFile(fileB);
            if (a == null || b == null)
                return;
            
            m_merge = b;

            Compare(a, b);
        }

        public AssemblyDefinition LoadFile(string fileName)
        {
            try
            {
                return AssemblyFactory.GetAssembly(fileName);
            }
            catch
            {
                return null;
            }
        }

        public void RegisterCollector(AbstractCollector collector, string id)
        {
            m_registeredCollectors.Add(id, collector);
            m_collecterList.Add(collector);
        }

        public Dictionary<String, AbstractCollector> RegisteredCollectors
        {
            get { return m_registeredCollectors; }
        }

        public int Compare(AssemblyDefinition a, AssemblyDefinition b)
        {
            MatchResult<TypeDefinition> result = Collector.CollectMatches<TypeDefinition>(a, b, StructureMatch.Type);

            int changes = 0;

            foreach (EntityVersion<TypeDefinition> version in result.CommonElements)
            {
                // Notify
                foreach( AbstractCollector collector in m_collecterList )
                {
                    collector.OnCompareTypeBegin( version.Original, version.Target );
                }

                int change = 0;
                // Attributes.
                change += CompareMethods(version);  // includes constructors.
                change += CompareFields(version);
                change += CompareProperties(version);

                // Notify
                foreach (AbstractCollector collector in m_collecterList)
                {
                    collector.OnCompareTypeEnd(change == 0 ? ChangeResult.Same : ChangeResult.Changed, version.Original, version.Target);
                }
                changes += change;
            }

            foreach( AbstractCollector collector in m_collecterList )
            {
                collector.OnNewTypes( result.NewElements );
                // New Methods, Properties, Fields
            }

            foreach( AbstractCollector collector in m_collecterList )
            {
                collector.OnNewTypes( result.DeletedElements );
                // Deleted Methods, Properties, Fields
            }

            changes += result.NewElements.Count + result.DeletedElements.Count;
            return changes;
        }
        
        private int CompareMethods(EntityVersion<TypeDefinition> version)
        {
            MatchResult<MethodDefinition> result = Collector.CollectMatches<MethodDefinition>(version.Original,version.Target,StructureMatch.Method);

            int changes = 0;

            foreach (EntityVersion<MethodDefinition> mVersion in result.CommonElements)
            {
                foreach( AbstractCollector collector in m_collecterList )
                {
                    collector.OnCompareMethodBegin( mVersion.Original, mVersion.Target );
                }

                int change = CompareMethodDeep(mVersion);

                foreach (AbstractCollector collector in m_collecterList)
                {
                    collector.OnCompareMethodEnd(change == 0 ? ChangeResult.Same:ChangeResult.Changed, mVersion.Original, mVersion.Target);
                }

                changes += change;
            }
            
            foreach( AbstractCollector collector in m_collecterList )
            {
                collector.OnNewMethods( result.NewElements );
            }

            foreach( AbstractCollector collector in m_collecterList )
            {
                collector.OnDeletedMethods( version.Target, result.DeletedElements  );
            }

            changes += result.NewElements.Count + result.DeletedElements.Count;
            return changes;
        }

        private int CompareProperties(EntityVersion<TypeDefinition> version)
        {
            int changes = 0;
            MatchResult<PropertyDefinition> result = Collector.CollectMatches<PropertyDefinition>(version.Original, version.Target, StructureMatch.Property);
            foreach (EntityVersion<PropertyDefinition> mVersion in result.CommonElements)
            {
                foreach (AbstractCollector collector in m_collecterList)
                {
                    collector.OnCompareProperties(mVersion.Original, mVersion.Target);
                }

                // CompareProperty
            }

            foreach (AbstractCollector collector in m_collecterList)
            {
                collector.OnNewProperties(result.NewElements);
            }

            foreach (AbstractCollector collector in m_collecterList)
            {
                collector.OnDeletedProperties(result.DeletedElements);
            }

            //PropertyDefinition
            //mVersion.Original.Attributes,
            ////mVersion.Original.Constant,
            //mVersion.Original.CustomAttributes,
            ////mVersion.Original.DeclaringType,
            ////mVersion.Original.GetMethod,
            ////mVersion.Original.HasConstant,
            //mVersion.Original.Parameters,
            //mVersion.Original.PropertyType,
            ////mVersion.Original.SetMethod,
            //mVersion.Original.HasDefault
            changes += result.NewElements.Count + result.DeletedElements.Count;
            return changes;
        }

        private int CompareFields(EntityVersion<TypeDefinition> version)
        {
            int changes = 0;
            MatchResult<FieldDefinition> result = Collector.CollectMatches<FieldDefinition>(version.Original, version.Target, StructureMatch.Field);
            foreach (EntityVersion<FieldDefinition> mVersion in result.CommonElements)
            {
                foreach (AbstractCollector collector in m_collecterList)
                {
                    collector.OnCompareFields(mVersion.Original, mVersion.Target );
                }
            }

            foreach (AbstractCollector collector in m_collecterList)
            {
                collector.OnNewFields(result.NewElements);
            }

            foreach (AbstractCollector collector in m_collecterList)
            {
                collector.OnDeletedFields(result.DeletedElements);
            }

            changes += result.NewElements.Count + result.DeletedElements.Count;
            return changes;
        }

        private string CompareItemProperties(object instA, object instB, params string[] properties)
        {
            Type a = instA.GetType();
            Type b = instB.GetType(); 
            StringBuilder buffer = new StringBuilder();
            foreach (string property in properties)
            {
                System.Reflection.PropertyInfo pi = a.GetProperty(property);
                buffer.Append(pi.GetValue(instA, null) == pi.GetValue(instB, null) ? "" : property).Append(";");
            }
            return buffer.ToString();
        }

        private string CompareItems(MethodDefinition a, MethodDefinition b)
        {
            StringBuilder buffer = new StringBuilder();
            foreach (MethodAttributes val in Enum.GetValues(typeof(MethodAttributes)))
            {
                buffer.Append((a.Attributes | val) == (b.Attributes | val) ? "" : Enum.GetName(typeof(MethodAttributes), val)).Append(";");
            }
            return buffer.ToString();
        }

        private string CompareItems(FieldDefinition a, FieldDefinition b)
        {
            StringBuilder buffer = new StringBuilder();
            foreach (FieldAttributes val in Enum.GetValues(typeof(FieldAttributes)))
            {
                buffer.Append((a.Attributes | val) == (b.Attributes| val) ? "" : Enum.GetName(typeof(FieldAttributes), val)).Append(";");
            }
            return buffer.ToString();
        }

        private string CompareItems(PropertyDefinition a, PropertyDefinition b)
        {
            StringBuilder buffer = new StringBuilder();
            foreach (PropertyAttributes val in Enum.GetValues(typeof(PropertyAttributes)))
            {
                buffer.Append((a.Attributes | val) == (b.Attributes | val) ? "" : Enum.GetName(typeof(PropertyAttributes), val)).Append(";");
            }
            return buffer.ToString();
        }

        private string CompareItems(TypeDefinition a, TypeDefinition b)
        {
            StringBuilder buffer = new StringBuilder();
            foreach (TypeAttributes val in Enum.GetValues(typeof(TypeAttributes)))
            {
                buffer.Append((a.Attributes | val) == (b.Attributes | val) ? "" : Enum.GetName(typeof(TypeAttributes), val)).Append(";");
            }
            return buffer.ToString();
        }

        private string EncodeDifferences(Diff.Item[] items)
        {
            if (items == null)
                return "";
            StringBuilder buffer = new StringBuilder();
            foreach (Diff.Item item in items)
            {
                int delA = item.deletedA;
                int addB = item.insertedB;
                int starta = item.StartA;
                int startb = item.StartB;
                if (delA > 0 && addB > 0)
                {
                    // Before[starta], After[startb]
                    buffer.Append("*").Append(starta).Append(",").Append(startb);
                }
                else if (delA > 0 && addB == 0)
                {
                    // Before[starta]
                    buffer.Append("-").Append(starta);
                }
                else if (addB > 0 && delA == 0)
                {
                    // After[startb]
                    buffer.Append("+").Append(startb);
                }
                buffer.Append(";");
            }
            return buffer.ToString();
        }

        private int CompareMethodDeep(EntityVersion<MethodDefinition> mVersion)
        {
            // Signature;
            SectionChange signature = new SectionChange((int)SignatureChange.Same, "");
            
            // Body
            Diff.Item[] items = (mVersion.Original.Body == null || mVersion.Target.Body == null) ? null : CompareBody(mVersion);
            SectionChange body = new SectionChange((int)ElementChange.Modified, EncodeDifferences( items ));

            if (items == null)
            {
                return 0;
            }

            // ReturnType
            PropertyChange rType = mVersion.Original.ReturnType == mVersion.Target.ReturnType?
                PropertyChange.Same : PropertyChange.Different;

            // missing some properties still...
            SectionChange returnType = new SectionChange((int)rType, "");
            
            // Attributes
            string attributesdiff = CompareItems(mVersion.Original, mVersion.Target);
            SectionChange attributes = new SectionChange(attributesdiff == "" ? (int)PropertyChange.Same : (int)PropertyChange.Different, attributesdiff);

            // Properties
            //string propertiesdiff = CompareItemProperties(mVersion.Original, mVersion.Target, "CallingConvention", "ExplicitThis",
            //    "HasBody", "HasSecurity", "HasThis", "ImplAttributes", "IsAbstract", "IsAssembly");
            //SectionChange properties = new SectionChange(propertiesdiff == "" ? (int)PropertyChange.Same : (int)PropertyChange.Different, propertiesdiff);
            SectionChange properties = new SectionChange((int)PropertyChange.Same, "");

            // CustomAttributes;
            SectionChange customAttributes = new SectionChange((int)ElementChange.Same, "");
            // Overrides;
            SectionChange overrides = new SectionChange((int)ElementChange.Same, "");
            
            // Parameters 
            // Because signature matching includes parameters, number of parameters is same.
            string[] parameters_diff = new string[mVersion.Original.Parameters.Count];
            for (int i = 0; i < mVersion.Original.Parameters.Count; i++)
            {
                ParameterDefinition pa = mVersion.Original.Parameters[i];
                ParameterDefinition pb = mVersion.Target.Parameters[i];
                string d = CompareItemProperties(pa, pb, "Constant", "HasConstant", "HasDefault", "IsIn", "IsOptional",
                    "IsOut", "Name", "ParameterType", "Sequence");
                parameters_diff[i] = i + ":" + d;
            }
            string par = string.Join(";", parameters_diff);
            SectionChange parameters = new SectionChange(par == "" ? (int)PropertyChange.Same : (int)PropertyChange.Different,par);

            // Generic Parameters Method<T<A,B>> where T:Interface.
            string[] genericparameters_diff = new string[mVersion.Original.Parameters.Count];
            for (int i = 0; i < mVersion.Original.GenericParameters.Count; i++)
            {
                GenericParameter ga = mVersion.Original.GenericParameters[i];
                GenericParameter gb = mVersion.Target.GenericParameters[i];

                // !!! ga.GenericParameters
                string d = CompareItemProperties(ga, gb, "FullName", "HasDefaultConstructorConstraint", "HasNotNullableValueTypeConstraint", "HasReferenceTypeConstraint", "HasReferenceTypeConstraint",
                    "IsContravariant", "IsCovariant", "IsValueType", "Name", "Position");
                genericparameters_diff[i] = i + ":" + d;
            }
            string gpar = string.Join(";", genericparameters_diff);
            SectionChange genericparameters = new SectionChange(gpar == "" ? (int)PropertyChange.Same : (int)PropertyChange.Different, gpar);

            return 1;
        }

        private string EncodeString(MethodDefinition md)
        {
            if (md.Body == null)
                return "";
            StringBuilder code = new StringBuilder();
            foreach (Instruction i in md.Body.Instructions)
            {
                code.Append(i.OpCode.Name);
                code.Append(" ");
                code.Append(i.Operand is String ? String.Format("\"{0}\"", i.Operand) : i.Operand);
                code.Append("\n");
            }
            return code.ToString();
        }

        private Diff.Item[] CompareBody(EntityVersion<MethodDefinition> mVersion)
        {
            Diff.Item[] items = null;

            //MethodTokens tokensA = GetTokens(mVersion.Original);
            //MethodTokens tokensB = GetTokens(mVersion.Target);

            //string strA = tokensA.ToString();
            //string strB = tokensB.ToString();
            string strA = EncodeString(mVersion.Original);
            string strB = EncodeString(mVersion.Target);
            if (strA == strB)
                return items;

            items = Diff.DiffText(strA, strB);
            if (items.Length > 0)
            {
                //Console.Write("*"+CodeProperties.GetClassName(mVersion.Target.DeclaringType)+"::"+CodeProperties.GetMethodNameWithParametersTight(mVersion.Target,true));
                //Console.WriteLine(":" + items.Length);
                foreach (Diff.Item item in items)
                {
                    #region stuff
                    // Console.Write("[");
                    int delA = item.deletedA;
                    int addB = item.insertedB;
                    int starta = item.StartA;
                    int startb = item.StartB;
                    if (delA > 0 && addB > 0)
                    {
                        // Change
                        //Console.Write("before:"+tokensA.Lines[starta]);
                       // Console.Write("after:" + tokensB.Lines[startb]);
                    }
                    else if (delA > 0 && addB == 0)
                    {
                       // Console.Write("-:" + tokensA.Lines[starta]);
                    }
                    else if (addB > 0 && delA == 0)
                    {
                        //Console.Write("+:" + tokensB.Lines[startb]);
                    }
                    #endregion
                    // Console.Write("];;;");
                }
               // Console.WriteLine("");
            }
            return items;
        }
        public class TokenStatement
        {
            public MethodDefinition MD;
            public List<CILFlow.TypedToken> Tokens;
            public TokenStatement(System.Collections.ArrayList list, MethodDefinition md)
            {
                this.Tokens = new List<CILFlow.TypedToken>();
                foreach (CILFlow.TypedToken tok in list)
                {
                    this.Tokens.Add(tok);
                }
                this.MD = md;
            }
            public override string ToString()
            {
                StringBuilder sb = new StringBuilder();
                foreach (object t in Tokens)
                {
                    if( !(t is string) )
                    {
                        TypedToken token = (TypedToken)t;
                        string operand = Operand(this.MD, token.Instruction);
                        sb.Append("#");
                        if (token.Instruction.OpCode == OpCodes.Ldstr)
                        {
                            sb.Append(System.Text.RegularExpressions.Regex.Escape(operand));
                        }
                        else
                            sb.Append(token.Instruction.OpCode).Append(",").Append(operand);
                        sb.Append("#");
                    }
                }
                return sb.ToString();
            }
        }

        static string Operand(MethodDefinition md, Instruction i)
        {
            object operand = i.Operand;
            if( operand == null )
                operand = CILUtil.Entity.GetOperand(md, i);
            if (operand != null && !(operand is Instruction))
                return operand.ToString();
            return "none";
        }

        public class MethodTokens
        {
            public List<TokenStatement> Lines = new List<TokenStatement>();
            public override string ToString()
            {
                StringBuilder sb = new StringBuilder();
                foreach (TokenStatement tokenLine in this.Lines)
                {
                    sb.Append(tokenLine.ToString()).Append('\n');
                }
                return sb.ToString();
            }
        }

        public MethodTokens GetTokens(MethodDefinition md)
        {
            CILFlowGraph flowgraph = CILMethodBlockConstructor.ConstructFlow(md);
            Dictionary<CILInstructionBlock, PopTreeList> blockPopTreeListMap = new Dictionary<CILInstructionBlock, PopTreeList>();
            foreach (CILInstructionBlock block in flowgraph.Blocks)
            {
                PopTreeList treeList = PopTreeListBuilder.Construct(md,block);
                blockPopTreeListMap[block] = treeList;
            }
            MethodTokens tokens = new MethodTokens();

            foreach (CILInstructionBlock block in flowgraph.Blocks)
            {
                PopTreeList treeList = (PopTreeList)blockPopTreeListMap[block];
                foreach (PopTree tree in treeList.PopTrees)
                {
                    System.Collections.ArrayList a = tree.Flatten();
                    tokens.Lines.Add( new TokenStatement(a,md) );
                }
            }
            return tokens;
        }

        private void X(MethodDefinition md)
        {
            CILFlowGraph flowgraph = CILMethodBlockConstructor.ConstructFlow(md);
            Dictionary<CILInstructionBlock, PopTreeList> blockPopTreeListMap = new Dictionary<CILInstructionBlock, PopTreeList>();
            foreach (CILInstructionBlock block in flowgraph.Blocks)
            {
                PopTreeList treeList = PopTreeListBuilder.Construct(md,block);
                blockPopTreeListMap[block] = treeList;
            }
            Console.WriteLine(md.Name);
            foreach (CILInstructionBlock block in flowgraph.Blocks)
            {
                Console.WriteLine("{");
                PopTreeList treeList = (PopTreeList)blockPopTreeListMap[block];
                foreach (PopTree tree in treeList.PopTrees)
                {
                    System.Collections.ArrayList a = tree.Flatten();
                    foreach (object token in a)
                    {
                        if (token is string)
                            Console.Write(token);
                        else
                        {
                            TypedToken type = (TypedToken)token;

                            object operand = type.Instruction.Operand;
                            if( operand == null )
                                operand = CILUtil.Entity.GetOperand(md, type.Instruction);
                            if (operand != null && !(operand is Instruction))
                            {
                                if (operand is VariableDefinition)
                                {
                                    VariableDefinition def = (VariableDefinition)operand;
                                    
                                }
                                Console.Write(operand);
                            }
                            else
                            {
                                switch( type.Instruction.OpCode.Code )
                                {
                                    case Code.Ceq:
                                        Console.Write("==");
                                        break;
                                    case Code.Ldnull:
                                        Console.Write("null");
                                        break;
                                    default:
                                        Console.Write(type.Instruction.OpCode.ToString());
                                        break;

                                }
                                
                            }
                        }
                    }
                    Console.WriteLine(";");
                }
                Console.WriteLine("}");
            }
        }
    }
}
